package com.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    public void set(String key , Object value , Long time , TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key , JSONUtil.toJsonStr(value) ,
                time , unit);
    }

    // 逻辑过期
    public void setWithLogicalExpire(String key , Object value , Long time , TimeUnit unit) {

        // 传入逻辑过期时间
        RedisDate redisDate = new RedisDate();
        redisDate.setDate(value);
        redisDate.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        stringRedisTemplate.opsForValue().set(key , JSONUtil.toJsonStr(value));
    }


    // 设空值解决缓存穿透
    public <R , ID> R queryWithPassThrough(String keyPrefix , ID id ,
                                           Class<R> type , Function<ID , R> dbFallBack ,
                                           Long time , TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.在Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        // 2.缓存命中，返回查询到的信息
        if( StrUtil.isNotBlank(json) ) {
            // 将json数据转化为对象
            return JSONUtil.toBean(json , type);
        }

        // 3.判断缓存命中的是否是"",因为在后续过程，
        //        如果数据库中也未有该信息，会存储""进入Redis,预防缓存穿透
        if( json != null ) {
            return null;
        }

        // 4.缓存未命中，查询数据库
        R r = dbFallBack.apply(id);

        // 5.数据库中不存在该信息，则返回错误信息给客户，并存入""进入Redis
        if( r == null ) {
            stringRedisTemplate.opsForValue().set(key , "" , 30 , TimeUnit.MINUTES);
            return null;
        }
        // 6.数据库中存在该信息，则返回该信息，并存储进入Redis
        this.set(key , r , time , unit);
        return r;
    }


    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }


    private static final ExecutorService CACHE_REBUILD_EXECUTOR =
            Executors.newFixedThreadPool(10);

    // 逻辑过期
    public <R , ID> R queryWithLogicalExpire(String keyPrefix , String lockPrefix , ID id ,
                                             Class<R> type , Function<ID , R> dbFallBack ,
                                             Long time , TimeUnit unit) {

        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        //若为空，则直接返回错误信息
        if( StrUtil.isBlank(json) ) {
            return null;
        }

        RedisDate redisDate = JSONUtil.toBean(json, RedisDate.class);
        R r = JSONUtil.toBean( (JSONObject) redisDate.getDate() , type);

        LocalDateTime expireTime = redisDate.getExpireTime();

        // 如果过期时间在当前时间之后，返回
        if( expireTime.isAfter(LocalDateTime.now()) ) {
            return r;
        }

        String lockKey = lockPrefix + id;
        boolean isLock = tryLock(lockKey);

        // 判断是否拿到锁
        if( isLock ) {

            //做二次效验，检查拿到锁之后，过期时间是否还是已经过期
            if( expireTime.isAfter(LocalDateTime.now()) ) {
                return r;
            }

            CACHE_REBUILD_EXECUTOR.submit( () -> {
                try {
                    R r1 = dbFallBack.apply(id);
                    this.setWithLogicalExpire(key , r1 , time , unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }

        //如果获取失败，返回过期时间
        return r;
    }
}
